﻿#region File and License Information
/*
<File>
	<Copyright>Copyright © 2007, Daniel Vaughan. All rights reserved.</Copyright>
	<License see="prj:///Documentation/License.txt"/>
	<Owner Name="Daniel Vaughan" Email="dbvaughan@gmail.com"/>
	<CreationDate>2009-03-01 18:44:00Z</CreationDate>
	<LastSubmissionDate>$Date: $</LastSubmissionDate>
	<Version>$Revision: $</Version>
</File>
*/
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.Threading;

using log4net;

using Microsoft.Practices.Unity;

namespace DanielVaughan.ServiceModel
{
	/// <summary>
	/// Default implementation of the <see cref="IChannelManager"/>.
	/// Contains methods that enable the creation of channels that implement
	/// specific service interfaces. This class is designed to be used with
	/// Unity in order for transparent factory production of service channels.
	/// This class can be used to provide Unity with a delegate
	/// for creating channels implementing the specified service interface.
	/// </summary>
	public class ChannelManagerSingleton : IChannelManager
	{
		#region Singleton implementation

		ChannelManagerSingleton()
		{
		}

		public static ChannelManagerSingleton Instance
		{
			get
			{
				return Nested.instance;
			}
		}

		class Nested
		{
			// Explicit static constructor to tell C# compiler
			// not to mark type as beforefieldinit
			static Nested()
			{
			}

			internal static readonly ChannelManagerSingleton instance = new ChannelManagerSingleton();
		}

		#endregion

		//readonly LoggerSingleton log = LoggerSingleton.Instance;
		static readonly ILog log = LogManager.GetLogger(typeof(ChannelManagerSingleton));
		readonly Dictionary<Type, object> channels = new Dictionary<Type, object>();
		/* LockRecursionPolicy.SupportsRecursion may not be required. It has been noticed 
		 * that recursion is occuring occasionally, and SupportsRecursion has been assigned 
		 * to prevent this problem. In short, it's a hack for now. */
		readonly ReaderWriterLockSlim channelsLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
		readonly Dictionary<DuplexChannelKey, object> duplexChannels = new Dictionary<DuplexChannelKey, object>();
		readonly ReaderWriterLockSlim duplexChannelsLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
		readonly string channelIdentifier = Assembly.GetExecutingAssembly().GetName().Name;

		/// <summary>
		/// Creates or retrieves a cached service channel.
		/// <example>
		/// var staticFactoryConfiguration = unityContainer.Configure<IStaticFactoryConfiguration>();
		///	Debug.Assert(staticFactoryConfiguration != null);
		///	if (staticFactoryConfiguration != null)
		///	{
		///		staticFactoryConfiguration.RegisterFactory<IYourService>(
		///			ChannelManagerSingleton.Instance.GetChannel<IYourService>);
		/// 
		///		/* Attempt to retrieve and use the service. */
		///		var yourService = UnityResolver.UnityContainer.Resolve<IYourService>();
		///		yourService.DoSomething();
		/// }
		/// </example>
		/// </summary>
		/// <param name="unityContainer">The unity container.</param>
		/// <returns>A channel of the specified type.</returns>
		/// <exception cref="CommunicationException">Occurs if the service implements <see cref="IServiceContract"/>, 
		/// and the call to InitiateConnection results in a <code>CommunicationException</code></exception>
		/// <exception cref="SecurityNegotiationException">Occurs if the service implements <see cref="IServiceContract"/>, 
		/// and the call to InitiateConnection results in a <code>SecurityNegotiationException</code></exception>
		public object GetChannel<TChannel>(IUnityContainer unityContainer)
		{
			return GetChannel<TChannel>();
		}

		public TChannel GetChannel<TChannel>()
		{
			Type serviceType = typeof(TChannel);
			object service;

			channelsLock.EnterUpgradeableReadLock();
			try
			{
				if (!channels.TryGetValue(serviceType, out service))
				{	/* Value not in cache, therefore we create it. */
					channelsLock.EnterWriteLock();
					try
					{
						/* We don't cache the factory as it contains a list of channels 
						 * that aren't removed if a fault occurs. */
						var channelFactory = new ChannelFactory<TChannel>("*");

						service = channelFactory.CreateChannel();
						var communicationObject = (ICommunicationObject)service;
						communicationObject.Faulted += OnChannelFaulted;
						channels.Add(serviceType, service);
						communicationObject.Open(); /* Explicit opening of the channel 
													 * avoids a performance hit.  */
						ConnectIfClientService(service, serviceType);
					}
					finally
					{
						channelsLock.ExitWriteLock();
					}
				}
			}
			finally
			{
				channelsLock.ExitUpgradeableReadLock();
			}

			return (TChannel)service;
		}

		public TChannel GetDuplexChannel<TChannel>(object callbackInstance)
		{
			if (callbackInstance == null)
			{
				throw new ArgumentNullException("callbackInstance");
			}

			Type serviceType = typeof(TChannel);
			object service;
			var key = new DuplexChannelKey { ServiceType = serviceType, CallBackInstance = callbackInstance };

			duplexChannelsLock.EnterUpgradeableReadLock();
			try
			{
				if (!duplexChannels.TryGetValue(key, out service))
				{	/* Value not in cache, therefore we create it. */
					duplexChannelsLock.EnterWriteLock();
					try
					{
						var context = new InstanceContext(callbackInstance);
						/* We don't cache the factory as it contains a list of channels 
						 * that aren't removed if a fault occurs. */
						var channelFactory = new DuplexChannelFactory<TChannel>(context, "*");

						service = channelFactory.CreateChannel();
						var communicationObject = (ICommunicationObject)service;
						communicationObject.Faulted += OnDuplexChannelFaulted;
						duplexChannels.Add(key, service);
						communicationObject.Open(); /* Explicit opening of the channel 
													 * avoids a performance hit.  */
						ConnectIfClientService(service, serviceType);
					}
					finally
					{
						duplexChannelsLock.ExitWriteLock();
					}
				}
			}
			finally
			{
				duplexChannelsLock.ExitUpgradeableReadLock();
			}

			return (TChannel)service;
		}

		/// <summary>
		/// Attempts to excercise the <code>IServiceContract.InitiateConnection</code> method
		/// on the specified service if that service is an <see cref="IServiceContract"/>.
		/// </summary>
		/// <param name="service">The service to attempt a connection.</param>
		/// <param name="serviceType">Type of the service for logging purposes.</param>
		/// <exception cref="CommunicationException">Occurs if the service implements <see cref="IServiceContract"/>, 
		/// and the call to InitiateConnection results in a <code>CommunicationException</code></exception>
		/// <exception cref="SecurityNegotiationException">Occurs if the service implements <see cref="IServiceContract"/>, 
		/// and the call to InitiateConnection results in a <code>SecurityNegotiationException</code></exception>
		void ConnectIfClientService(object service, Type serviceType)
		{
			var clientService = service as IServiceContract;
			if (clientService != null)
			{
				string response;
				try
				{
					response = clientService.InitiateConnection(channelIdentifier);
				}
				catch (Exception ex)
				{
					log.Warn("Unable to connect to IServiceContract instance. Type: " + serviceType, ex);
					throw;
				}
				log.Info(string.Format("Received response from {0} \"{1}\"", serviceType, response));
			}
		}

		/// <summary>
		/// Called when a channel fault occurs. The channel is removed from the channels cache
		/// so that the next request to retrieve the channel will see it recreated.
		/// </summary>
		/// <param name="sender">The <see cref="ICommunicationObject"/> sender.</param>
		/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
		void OnChannelFaulted(object sender, EventArgs e)
		{
			log.Warn("Channel faulted.");

			var communicationObject = (ICommunicationObject)sender;

			try
			{
				communicationObject.Abort();
			}
			catch (Exception ex)
			{
				log.Warn("Unable to abort channel.", ex);
			}

			channelsLock.EnterWriteLock();
			try
			{
				communicationObject.Faulted -= OnChannelFaulted;

				var keys = from pair in channels
						   where pair.Value == communicationObject
						   select pair.Key;

				/* Remove all items matching the channel. 
				 * This is somewhat defensive as there should only be one instance 
				 * of the channel in the channel dictionary. */
				foreach (var key in keys.ToList())
				{
					channels.Remove(key);
				}
			}
			finally
			{
				channelsLock.ExitWriteLock();
			}
		}

		void OnDuplexChannelFaulted(object sender, EventArgs e)
		{
			log.Warn("Duplex channel faulted.");

			var communicationObject = (ICommunicationObject)sender;

			try
			{
				communicationObject.Abort();
			}
			catch (Exception ex)
			{
				log.Warn("Unable to abort channel.", ex);
			}

			duplexChannelsLock.EnterWriteLock();
			try
			{
				communicationObject.Faulted -= OnDuplexChannelFaulted;

				var keys = from pair in duplexChannels
						   where pair.Value == communicationObject
						   select pair.Key;

				/* Remove all items matching the channel. 
				 * This is somewhat defensive as there should only be one instance 
				 * of the channel in the channel dictionary. */
				foreach (var key in keys.ToList())
				{
					duplexChannels.Remove(key);
				}
			}
			finally
			{
				duplexChannelsLock.ExitWriteLock();
			}
		}

		class DuplexChannelKey
		{
			public object CallBackInstance { get; set; }
			public Type ServiceType { get; set; }

			public override bool Equals(object obj)
			{
				if (ReferenceEquals(null, obj))
				{
					return false;
				}
				if (ReferenceEquals(this, obj))
				{
					return true;
				}
				if (obj.GetType() != typeof(DuplexChannelKey))
				{
					return false;
				}
				return Equals((DuplexChannelKey)obj);
			}

			public bool Equals(DuplexChannelKey obj)
			{
				if (ReferenceEquals(null, obj))
				{
					return false;
				}
				if (ReferenceEquals(this, obj))
				{
					return true;
				}
				return Equals(obj.CallBackInstance, CallBackInstance) && Equals(obj.ServiceType, ServiceType);
			}

			public override int GetHashCode()
			{
				unchecked
				{
					return ((CallBackInstance != null ? CallBackInstance.GetHashCode() : 0) * 397) 
						^ (ServiceType != null ? ServiceType.GetHashCode() : 0);
				}
			}
		}

	}
}
